﻿namespace Cellfish.Toolbox.Facts.Streams
{
    using System;
    using System.IO;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;

    using Cellfish.Test;
    using Cellfish.Toolbox.Streams;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class ProducerConsumerStreamFacts
    {
        private ProducerConsumerStream stream = new ProducerConsumerStream();

        private byte[] someData = UTF8Encoding.UTF8.GetBytes("SomeData");

        private byte[] buffer = new byte[128];

        [TestCleanup]
        public void Cleanup()
        {
            this.stream.Dispose();
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_it_supports_read()
        {
            Assert.IsTrue(this.stream.CanRead);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_it_supports_write()
        {
            Assert.IsTrue(this.stream.CanWrite);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_it_does_not_support_seaking()
        {
            Assert.IsFalse(this.stream.CanSeek);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_getting_length_Then_NotSupportedException_is_thrown()
        {
            long dummyLength;
            Ensure.Throws<NotSupportedException>(() => dummyLength = this.stream.Length);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_getting_position_Then_NotSupportedException_is_thrown()
        {
            long dummyPosition;
            Ensure.Throws<NotSupportedException>(() => dummyPosition = this.stream.Position);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_setting_position_Then_NotSupportedException_is_thrown()
        {
            Ensure.Throws<NotSupportedException>(() => this.stream.Position = 42);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_seeking_Then_NotSupportedException_is_thrown()
        {
            Ensure.Throws<NotSupportedException>(() => this.stream.Seek(42, SeekOrigin.Current));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_setting_length_Then_NotSupportedException_is_thrown()
        {
            Ensure.Throws<NotSupportedException>(() => this.stream.SetLength(42));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_reading_with_null_buffer_Then_ArgumentNullException_is_thrown()
        {
            Ensure.Throws<ArgumentNullException>(() => this.stream.ReadAsync(null, 0, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_reading_with_negative_offset_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.ReadAsync(this.buffer, -1, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_reading_with_offset_past_end_of_buffer_buffer_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.ReadAsync(this.buffer, this.buffer.Length, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_reading_with_negative_length_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.ReadAsync(this.buffer, 0, -1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_reading_with_length_past_end_of_buffer_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.ReadAsync(this.buffer, this.buffer.Length/2, this.buffer.Length/2 + 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_writing_with_null_buffer_Then_ArgumentNullException_is_thrown()
        {
            Ensure.Throws<ArgumentNullException>(() => this.stream.WriteAsync(null, 0, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_writing_with_negative_offset_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.WriteAsync(this.someData, -1, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_writing_with_offset_past_end_of_buffer_buffer_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.WriteAsync(this.someData, this.someData.Length, 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_writing_with_negative_length_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.WriteAsync(this.someData, 0, -1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_When_writing_with_length_past_end_of_buffer_Then_ArgumentOutOfRangeException_is_thrown()
        {
            Ensure.Throws<ArgumentOutOfRangeException>(() => this.stream.WriteAsync(this.someData, this.someData.Length / 2, this.someData.Length / 2 + 1));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_write_When_reading_more_than_available_data_Then_both_read_and_write_completes()
        {
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending write.");
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length);
            Assert.IsTrue(writeTask.Wait(TimeSpan.Zero), "Write is not completed.");
            Assert.IsTrue(readTask.Wait(TimeSpan.Zero), "Read is not completed.");
            Assert.IsFalse(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is pending operations.");
            Assert.AreEqual(this.someData.Length, readTask.Result, "Wrong number of bytes returned");
            Assert.AreEqual(UTF8Encoding.UTF8.GetString(this.someData), UTF8Encoding.UTF8.GetString(this.buffer, 0, readTask.Result));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_read_When_writing_more_than_available_data_Then_both_read_and_write_completes()
        {
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length);
            Assert.IsTrue(writeTask.Wait(TimeSpan.Zero), "Write is not completed.");
            Assert.IsTrue(readTask.Wait(TimeSpan.Zero), "Read is not completed.");
            Assert.IsFalse(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is pending operations.");
            Assert.AreEqual(this.someData.Length, readTask.Result, "Wrong number of bytes returned");
            Assert.AreEqual(UTF8Encoding.UTF8.GetString(this.someData), UTF8Encoding.UTF8.GetString(this.buffer, 0, readTask.Result));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_read_When_closing_Then_read_completes_with_zero_result()
        {
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            this.stream.Close();
            Assert.IsTrue(readTask.Wait(TimeSpan.Zero), "Read is not completed.");
            Assert.AreEqual(0, readTask.Result, "Wrong number of bytes returned");
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_write_When_closing_Then_ObjectDisposedException_is_thrown()
        {
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            this.stream.Close();
            Ensure.TaskFails<ObjectDisposedException>(writeTask);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_write_When_reading_parts_Then_reads_get_correct_data()
        {
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending write.");
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.someData.Length / 2);
            Assert.IsFalse(writeTask.Wait(TimeSpan.Zero), "Write is completed.");
            Assert.IsTrue(readTask.Wait(TimeSpan.Zero), "Read is not completed.");
            Assert.AreEqual(this.someData.Length / 2, readTask.Result, "Wrong number of bytes returned in first read");
            Assert.AreEqual(UTF8Encoding.UTF8.GetString(this.someData, 0, this.someData.Length / 2), UTF8Encoding.UTF8.GetString(this.buffer, 0, this.someData.Length / 2));
            this.buffer = new byte[512];
            readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length);
            Assert.IsTrue(writeTask.Wait(TimeSpan.Zero), "Write is not completed.");
            Assert.IsTrue(readTask.Wait(TimeSpan.Zero), "Read is not completed.");
            Assert.AreEqual(this.someData.Length - this.someData.Length / 2, readTask.Result, "Wrong number of bytes returned in first read");
            Assert.AreEqual(UTF8Encoding.UTF8.GetString(this.someData).Substring(this.someData.Length / 2), UTF8Encoding.UTF8.GetString(this.buffer, 0, this.someData.Length - this.someData.Length / 2));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_read_When_reading_Then_InvalidOperationException_is_thrown()
        {
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            Ensure.Throws<InvalidOperationException>(() => this.stream.ReadByte());
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_write_When_writing_Then_InvalidOperationException_is_thrown()
        {
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            Ensure.Throws<InvalidOperationException>(() => this.stream.WriteByte(42));
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_read_When_cancelling_Then_TaskCancelledException_is_thrown()
        {
            var cts = new CancellationTokenSource();
            var readTask = this.stream.ReadAsync(this.buffer, 0, this.buffer.Length, cts.Token);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            cts.Cancel();
            Ensure.TaskFails<TaskCanceledException>(readTask);
        }

        [TestMethod]
        public void Given_ProducerConsumerStream_with_pending_write_When_cancelling_Then_TaskCancelledException_is_thrown()
        {
            var cts = new CancellationTokenSource();
            var writeTask = this.stream.WriteAsync(this.someData, 0, this.someData.Length, cts.Token);
            Assert.IsTrue(this.stream.Pending.WaitOne(TimeSpan.Zero), "There is no pending read.");
            cts.Cancel();
            Ensure.TaskFails<TaskCanceledException>(writeTask);
        }

        [TestMethod]
        public void Given_disposed_ProducerConsumerStream_When_reading_Then_ObjectDisposedException_is_thrown()
        {
            this.stream.Dispose();
            Ensure.Throws<ObjectDisposedException>(() => this.stream.ReadAsync(this.buffer, 0, this.buffer.Length));
        }

        [TestMethod]
        public void Given_disposed_ProducerConsumerStream_When_writeing_Then_ObjectDisposedException_is_thrown()
        {
            this.stream.Dispose();
            Ensure.Throws<ObjectDisposedException>(() => this.stream.WriteAsync(this.someData, 0, this.someData.Length));
        }
    }
}
